Apprenez à implémenter l'estimation de la progression et la prédiction du temps d'achèvement avec le hook useFormStatus de React, améliorant l'expérience utilisateur dans les applications lourdes en données.
Estimation de la progression avec useFormStatus de React : Prédiction du temps d'achèvement
Le hook useFormStatus de React, introduit dans React 18, fournit des informations précieuses sur l'état de la soumission d'un formulaire. Bien qu'il n'offre pas directement d'estimation de la progression, nous pouvons exploiter ses propriétés et d'autres techniques pour fournir aux utilisateurs un retour significatif lors de soumissions de formulaires potentiellement longues. Cet article explore des méthodes pour estimer la progression et prédire le temps d'achèvement lors de l'utilisation de useFormStatus, résultant en une expérience plus engageante et conviviale.
Comprendre useFormStatus
Avant de plonger dans l'estimation de la progression, récapitulons rapidement le but de useFormStatus. Ce hook est conçu pour être utilisé à l'intérieur d'un élément <form> qui utilise la prop action. Il retourne un objet contenant les propriétés suivantes :
pending: Un booléen indiquant si le formulaire est en cours de soumission.data: Les données qui ont été soumises avec le formulaire (si la soumission a réussi).method: La méthode HTTP utilisée pour la soumission du formulaire (par ex., 'POST', 'GET').action: La fonction passée à la propactiondu formulaire.error: Un objet d'erreur si la soumission a échoué.
Bien que useFormStatus nous indique si le formulaire est en cours de soumission, il ne donne aucune information directe sur la progression de la soumission, surtout si la fonction action implique des opérations complexes ou longues.
Le défi de l'estimation de la progression
Le défi principal réside dans le fait que l'exécution de la fonction action est opaque pour React. Nous ne savons pas intrinsèquement où en est le processus. C'est particulièrement vrai pour les opérations côté serveur. Cependant, nous pouvons employer diverses stratégies pour surmonter cette limitation.
Stratégies pour l'estimation de la progression
Voici plusieurs approches que vous pouvez adopter, chacune avec ses propres compromis :
1. Server-Sent Events (SSE) ou WebSockets
La solution la plus robuste consiste souvent à pousser les mises à jour de la progression du serveur vers le client. Cela peut être réalisé en utilisant :
- Server-Sent Events (SSE) : Un protocole unidirectionnel (serveur-client) qui permet au serveur de pousser des mises à jour vers le client via une seule connexion HTTP. SSE est idéal lorsque le client a seulement besoin de *recevoir* des mises à jour.
- WebSockets : Un protocole de communication bidirectionnel qui fournit une connexion persistante entre le client et le serveur. Les WebSockets sont adaptés aux mises à jour en temps réel dans les deux sens.
Exemple (SSE) :
Côté serveur (Node.js) :
const express = require('express');
const app = express();
app.get('/progress', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
let progress = 0;
const interval = setInterval(() => {
progress += 10;
if (progress > 100) {
progress = 100;
clearInterval(interval);
res.write(`data: {"progress": ${progress}, "completed": true}\n\n`);
res.end();
} else {
res.write(`data: {"progress": ${progress}, "completed": false}\n\n`);
}
}, 500); // Simule une mise Ă jour de la progression toutes les 500ms
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Côté client (React) :
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const eventSource = new EventSource('/progress');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setProgress(data.progress);
if (data.completed) {
eventSource.close();
}
};
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
return (
<div>
<p>Progression : {progress}%</p>
</div>
);
}
export default MyComponent;
Explication :
- Le serveur définit les en-têtes appropriés pour SSE.
- Le serveur envoie des mises à jour de progression sous forme d'événements
data:. Chaque événement est un objet JSON contenant laprogressionet un drapeaucompleted. - Le composant React utilise
EventSourcepour écouter ces événements. - Le composant met à jour l'état (
progress) en fonction des événements reçus.
Avantages : Mises à jour de progression précises, retour en temps réel.
Inconvénients : Nécessite des modifications côté serveur, implémentation plus complexe.
2. Interrogation (Polling) avec un point de terminaison d'API
Si vous ne pouvez pas utiliser SSE ou WebSockets, vous pouvez implémenter l'interrogation. Le client envoie périodiquement des requêtes au serveur pour vérifier l'état de l'opération.
Exemple :
Côté serveur (Node.js) :
const express = require('express');
const app = express();
// Simule une tâche de longue durée
let taskProgress = 0;
let taskId = null;
app.post('/start-task', (req, res) => {
taskProgress = 0;
taskId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); // Génère un ID de tâche unique
// Simule un traitement en arrière-plan
const interval = setInterval(() => {
taskProgress += 10;
if (taskProgress >= 100) {
taskProgress = 100;
clearInterval(interval);
}
}, 500);
res.json({ taskId });
});
app.get('/task-status/:taskId', (req, res) => {
if (req.params.taskId === taskId) {
res.json({ progress: taskProgress });
} else {
res.status(404).json({ message: 'Task not found' });
}
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Côté client (React) :
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [taskId, setTaskId] = useState(null);
const startTask = async () => {
const response = await fetch('/start-task', { method: 'POST' });
const data = await response.json();
setTaskId(data.taskId);
};
useEffect(() => {
if (!taskId) return;
const interval = setInterval(async () => {
const response = await fetch(`/task-status/${taskId}`);
const data = await response.json();
setProgress(data.progress);
if (data.progress === 100) {
clearInterval(interval);
}
}, 1000); // Interroge toutes les secondes
return () => clearInterval(interval);
}, [taskId]);
return (
<div>
<button onClick={startTask} disabled={taskId !== null}>Démarrer la tâche</button>
{taskId && <p>Progression : {progress}%</p>}
</div>
);
}
export default MyComponent;
Explication :
- Le client démarre une tâche en appelant
/start-task, recevant untaskId. - Le client interroge ensuite périodiquement
/task-status/:taskIdpour obtenir la progression.
Avantages : Relativement simple à implémenter, ne nécessite pas de connexions persistantes.
Inconvénients : Peut être moins précis que SSE/WebSockets, introduit une latence due à l'intervalle d'interrogation, charge le serveur en raison de requêtes fréquentes.
3. Mises Ă jour optimistes et heuristiques
Dans certains cas, vous pouvez utiliser des mises à jour optimistes combinées à des heuristiques pour fournir une estimation raisonnable. Par exemple, si vous téléversez des fichiers, vous pouvez suivre le nombre d'octets téléversés côté client et estimer la progression en fonction de la taille totale du fichier.
Exemple (Téléversement de fichier) :
import React, { useState } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [file, setFile] = useState(null);
const handleFileChange = (event) => {
setFile(event.target.files[0]);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentage = Math.round((event.loaded * 100) / event.total);
setProgress(percentage);
}
});
xhr.open('POST', '/upload'); // Remplacez par votre point de terminaison de téléversement
xhr.send(formData);
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Upload complete!');
} else {
console.error('Upload failed:', xhr.status);
}
};
xhr.onerror = () => {
console.error('Upload failed');
};
} catch (error) {
console.error('Upload error:', error);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={!file}>Téléverser</button>
</form>
<p>Progression : {progress}%</p>
</div>
);
}
export default MyComponent;
Explication :
- Le composant utilise un objet
XMLHttpRequestpour téléverser le fichier. - L'écouteur d'événement
progresssurxhr.uploadest utilisé pour suivre la progression du téléversement. - Les propriétés
loadedettotalde l'événement sont utilisées pour calculer le pourcentage d'achèvement.
Avantages : Côté client uniquement, peut fournir un retour immédiat.
Inconvénients : La précision dépend de la fiabilité de l'heuristique, peut ne pas convenir à tous les types d'opérations.
4. Décomposer l'action en étapes plus petites
Si la fonction action effectue plusieurs étapes distinctes, vous pouvez mettre à jour l'interface utilisateur après chaque étape pour indiquer la progression. Cela nécessite de modifier la fonction action pour fournir des mises à jour.
Exemple :
import React, { useState } from 'react';
async function myAction(setProgress) {
setProgress(10);
await someAsyncOperation1();
setProgress(40);
await someAsyncOperation2();
setProgress(70);
await someAsyncOperation3();
setProgress(100);
}
function MyComponent() {
const [progress, setProgress] = useState(0);
const handleSubmit = async () => {
await myAction(setProgress);
};
return (
<div>
<form onSubmit={handleSubmit}>
<button type="submit">Soumettre</button>
</form>
<p>Progression : {progress}%</p>
</div>
);
}
export default MyComponent;
Explication :
- La fonction
myActionaccepte un rappelsetProgress. - Elle met à jour l'état de la progression à différents moments de son exécution.
Avantages : ContrĂ´le direct sur les mises Ă jour de la progression.
Inconvénients : Nécessite de modifier la fonction action, peut être plus complexe à implémenter si les étapes ne sont pas facilement divisibles.
Prédire le temps d'achèvement
Une fois que vous avez des mises à jour de la progression, vous pouvez les utiliser pour prédire le temps restant estimé. Une approche simple consiste à suivre le temps nécessaire pour atteindre un certain niveau de progression et à extrapoler pour estimer le temps total.
Exemple (Simplifié) :
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null);
const startTimeRef = useRef(null);
useEffect(() => {
if (progress > 0 && startTimeRef.current === null) {
startTimeRef.current = Date.now();
}
if (progress > 0) {
const elapsedTime = Date.now() - startTimeRef.current;
const estimatedTotalTime = (elapsedTime / progress) * 100;
const remainingTime = estimatedTotalTime - elapsedTime;
setEstimatedTimeRemaining(Math.max(0, remainingTime)); // Assure une valeur non négative
}
}, [progress]);
// ... (reste du composant et mises à jour de la progression comme décrit dans les sections précédentes)
return (
<div>
<p>Progression : {progress}%</p>
{estimatedTimeRemaining !== null && (
<p>Temps restant estimé : {Math.round(estimatedTimeRemaining / 1000)} secondes</p>
)}
</div>
);
}
export default MyComponent;
Explication :
- Nous stockons l'heure de début lorsque la progression est mise à jour pour la première fois.
- Nous calculons le temps écoulé et l'utilisons pour estimer le temps total.
- Nous calculons le temps restant en soustrayant le temps écoulé du temps total estimé.
Considérations importantes :
- Précision : C'est une prédiction *très* simplifiée. Les conditions du réseau, la charge du serveur et d'autres facteurs peuvent avoir un impact significatif sur la précision. Des techniques plus sophistiquées, comme le calcul de la moyenne sur plusieurs intervalles, peuvent améliorer la précision.
- Retour visuel : Indiquez clairement que le temps est une *estimation*. Afficher des fourchettes (par ex., "Temps restant estimé : 5-10 secondes") peut être plus réaliste.
- Cas limites : Gérez les cas où la progression est très lente au début. Évitez de diviser par zéro ou d'afficher des estimations excessivement grandes.
Combiner useFormStatus avec l'estimation de la progression
Bien que useFormStatus lui-même ne fournisse pas d'informations sur la progression, vous pouvez utiliser sa propriété pending pour activer ou désactiver l'indicateur de progression. Par exemple :
import React, { useState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (logique d'estimation de la progression des exemples précédents)
function MyComponent() {
const [progress, setProgress] = useState(0);
const { pending } = useFormStatus();
const handleSubmit = async (formData) => {
// ... (votre logique de soumission de formulaire, y compris les mises Ă jour de la progression)
};
return (
<form action={handleSubmit}>
<button type="submit" disabled={pending}>Soumettre</button>
{pending && <p>Progression : {progress}%</p>}
</form>
);
}
Dans cet exemple, l'indicateur de progression n'est affiché que lorsque le formulaire est en attente (c'est-à -dire, tant que useFormStatus.pending est true).
Meilleures pratiques et considérations
- Prioriser la précision : Choisissez une technique d'estimation de la progression appropriée au type d'opération effectuée. SSE/WebSockets fournissent généralement les résultats les plus précis, tandis que les heuristiques peuvent suffire pour des tâches plus simples.
- Fournir un retour visuel clair : Utilisez des barres de progression, des indicateurs de chargement ou d'autres indices visuels pour indiquer qu'une opération est en cours. Étiquetez clairement l'indicateur de progression et, le cas échéant, le temps restant estimé.
- Gérer les erreurs avec élégance : Si une erreur se produit pendant l'opération, affichez un message d'erreur informatif à l'utilisateur. Évitez de laisser l'indicateur de progression bloqué à un certain pourcentage.
- Optimiser les performances : Évitez d'effectuer des opérations coûteuses en calcul dans le thread de l'interface utilisateur, car cela peut avoir un impact négatif sur les performances. Utilisez des web workers ou d'autres techniques pour décharger le travail sur des threads d'arrière-plan.
- Accessibilité : Assurez-vous que les indicateurs de progression sont accessibles aux utilisateurs handicapés. Utilisez les attributs ARIA pour fournir des informations sémantiques sur la progression de l'opération. Par exemple, utilisez
aria-valuenow,aria-valueminetaria-valuemaxsur une barre de progression. - Localisation : Lors de l'affichage du temps restant estimé, tenez compte des différents formats d'heure et des préférences régionales. Utilisez une bibliothèque comme
date-fnsoumoment.jspour formater l'heure de manière appropriée pour la locale de l'utilisateur. - Internationalisation : Les messages d'erreur et autres textes doivent être internationalisés pour prendre en charge plusieurs langues. Utilisez une bibliothèque comme
i18nextpour gérer les traductions.
Conclusion
Bien que le hook useFormStatus de React ne fournisse pas directement de capacités d'estimation de la progression, vous pouvez le combiner avec d'autres techniques pour offrir aux utilisateurs un retour significatif lors des soumissions de formulaires. En utilisant SSE/WebSockets, l'interrogation, les mises à jour optimistes ou en décomposant les actions en étapes plus petites, vous pouvez créer une expérience plus engageante et conviviale. N'oubliez pas de prioriser la précision, de fournir un retour visuel clair, de gérer les erreurs avec élégance et d'optimiser les performances pour garantir une expérience positive à tous les utilisateurs, quel que soit leur emplacement ou leur contexte.